﻿#region Apache 2.0 License header

//   Copyright 2012 Lesley van Zijl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. 

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;

namespace FavourObjects
{
    /// <summary>
    /// Cache to store MemberInfo's to prevent using reflection to often.
    /// </summary>
    public class MemberInfoCache
    {
        /// <summary>
        /// Singleton instance.
        /// </summary>
        private static MemberInfoCache _instance;

        /// <summary>
        /// lock object.
        /// </summary>
        private static readonly Object LockObj = new Object();

        /// <summary>
        /// The internal cache.
        /// </summary>
        private readonly IDictionary<Method, MemberInfo> _registry = new Dictionary<Method, MemberInfo>();

        /// <summary>
        /// Private ctor.
        /// </summary>
        private MemberInfoCache()
        {
        }

        /// <summary>
        /// Returns the currently active memberinfo cache.
        /// </summary>
        public static MemberInfoCache ActiveCache
        {
            get
            {
                lock (LockObj)
                {
                    if (_instance == null)
                    {
                        _instance = new MemberInfoCache();
                    }
                }
                return _instance;
            }
        }

        /// <summary>
        /// Tries to get a method from the cache, if not able it uses reflection to retrieve it.
        /// </summary>
        /// <param name="owner">Owner type of the member.</param>
        /// <param name="name">Name of the method.</param>
        /// <param name="argumentTypes">Argument types of the method.</param>
        /// <returns>The method info that satisfies the specified properties.</returns>
        public MethodInfo GetMethod(Type owner, String name, params Type[] argumentTypes)
        {
            Contract.Requires(owner != null);
            Contract.Requires(!String.IsNullOrEmpty(name));
            
            var method = new Method
                             {
                                 Owner = owner,
                                 Name = name,
                                 Parameters = argumentTypes,
                                 MemberType = MemberTypes.Method
                             };

            MemberInfo memberInfo;
            if (!TryFindInRegistry(method, out memberInfo))
            {
                memberInfo = owner.GetMethod(name, argumentTypes);
                AddToRegistry(method, memberInfo);
            }
            
            return memberInfo as MethodInfo;
        }

        /// <summary>
        /// Tries to get a property from the cache, if not able it uses reflection to retrieve it.
        /// </summary>
        /// <param name="owner">Owner type of the member.</param>
        /// <param name="name">Name of the property</param>
        /// <param name="propertyType">The type of the property</param>
        /// <returns>The property info that satisfies the specified properties.</returns>
        public PropertyInfo GetProperty(Type owner, String name, Type propertyType)
        {
            Contract.Requires(owner != null);
            Contract.Requires(!String.IsNullOrEmpty(name));
            Contract.Requires(propertyType != null);

            var method = new Method
                             {
                                 Owner = owner,
                                 Name = name,
                                 Parameters = new[] {propertyType},
                                 MemberType = MemberTypes.Property
                             };

            MemberInfo memberInfo;
            if (!TryFindInRegistry(method, out memberInfo))
            {
                memberInfo = owner.GetProperty(name, propertyType);
                AddToRegistry(method, memberInfo);
            }

            return memberInfo as PropertyInfo;
        }

        /// <summary>
        /// Helper method that adds an item to the cache.
        /// </summary>
        /// <param name="method">Method used as key in the cache.</param>
        /// <param name="memberInfo">Member info that is used as value in the cache.</param>
        private void AddToRegistry(Method method, MemberInfo memberInfo)
        {
            if (memberInfo != null)
            {
                _registry[method] = memberInfo;
            }
        }

        /// <summary>
        /// Helper method that tries to find a method in the cache.
        /// </summary>
        /// <param name="method">Container of properties of the needed method.</param>
        /// <param name="memberInfo">out parameter in which the found member will be returned.</param>
        /// <returns>True if the member was in the cache, false otherwise.</returns>
        private bool TryFindInRegistry(Method method, out MemberInfo memberInfo)
        {
            memberInfo = null;

            IEnumerable<KeyValuePair<Method, MemberInfo>> items = _registry.Where(m => m.Key.Owner == method.Owner &&
                                                                                       m.Key.Name == method.Name &&
                                                                                       m.Key.MemberType ==
                                                                                       method.MemberType &&
                                                                                       method.Parameters != null &&
                                                                                       m.Key.Parameters.SequenceEqual(
                                                                                           method.Parameters.Select(
                                                                                               p => p)));

            if (items.Any())
            {
                memberInfo = items.First().Value;
            }

            return memberInfo != null;
        }
    }
}